﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Text;

namespace GermaneShanidze.Applications.Jokeri.LobbyDuplexService
{

    public class FileSenderServiceFactory : DuplexServiceFactory<FileSendService> { }

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class FileSendService : DuplexService
    {
        private List<SessionConnectionInfo> sessionConnections = new List<SessionConnectionInfo>();

        // Got code from http://stackoverflow.com/questions/1122483/c-random-string-generator
        private static Random random = new Random((int)DateTime.Now.Ticks);//thanks to McAden
        private string RandomString(int size)
        {
            StringBuilder builder = new StringBuilder();
            char ch;
            for (int i = 0; i < size; i++)
            {
                ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65)));
                builder.Append(ch);
            }

            return ( builder.ToString());
        }

        // This occurs when a user attempts to start a new session.  Make sure there isn't another existing session with the same username
        // If not, create a new session and assign a random session number.  Once created (or failed) we send a message to the pending host
        // user 
        private void CreateHostSession(HostSessionMessage hostSessionMessage)
        {
            HostSessionServerMessage hssm = new HostSessionServerMessage();

            SessionConnectionInfo sci = sessionConnections.Where(x => x.HostUserName == hostSessionMessage.Username).FirstOrDefault();
            if (sci != null)
            {
                hssm.Failed = true;
                hssm.SessionKey = sci.SessionKey;
            }
            else
            {
                SessionConnectionInfo sessionConnectionInfo = new SessionConnectionInfo();

                // ensure no duplicates
                do
                    sessionConnectionInfo.SessionKey = hostSessionMessage.Username;
                while (sessionConnections.Where(x => x.SessionKey == sessionConnectionInfo.SessionKey).Count() > 0);

                sessionConnectionInfo.HostUserName = hostSessionMessage.Username;
                sessionConnectionInfo.HostInternalSession = OperationContext.Current.Channel.SessionId;
                sessionConnections.Add(sessionConnectionInfo);
                hssm.SessionKey = sessionConnectionInfo.SessionKey;
                hssm.Failed = false;
            }

            PushMessageToClient(OperationContext.Current.Channel.SessionId, hssm);
        }

        // When a user attempts to connect to another, we check to see if the session number exists in our list.  If so, we add the
        // joining user's information to the session object, and send a message to the host to inform of the connected user.
        // The joining user will also receieve a message telling whether the connection succeeded or not.
        private void JoinSession(JoinSessionMessage joinMessage)
        {
            JoinSessionServerMessage jssm = new JoinSessionServerMessage();
            SessionConnectionInfo sci = sessionConnections.Where(x => x.SessionKey == joinMessage.SessionKey).FirstOrDefault();

            if (sci != null)
            {
                jssm.SessionNumber = sci.SessionKey;
                jssm.Hostname = sci.HostUserName;

                if (sci.UserConnected)
                    jssm.Failed = true;
                else
                {
                    sci.Join(joinMessage.Username, OperationContext.Current.Channel.SessionId);
                    jssm.Failed = false;
                    PushMessageToClient(sci.HostInternalSession,
                                        new ClientConnectedMessage() { Username = joinMessage.Username });
                }
            }
            else
            {
                jssm.Failed = true;
                jssm.SessionNumber = "";
                jssm.Hostname = "";
            }
            PushMessageToClient(OperationContext.Current.Channel.SessionId, jssm);
        }

        // A generic method that allows a host or the connected user to send a method to the other (whether it's file or chat related).
        // The method looks at the sender's internal session id, searchs for an active session with that ID.  If found it sends the
        // message to the other user.
        private void SendMessage(DuplexMessage msg)
        {
            ChatMessage message = (ChatMessage)msg;
            if(message.IsPublic)
            {
                // Send public message 
                foreach (SessionConnectionInfo sessionConnection in sessionConnections)
                {
                    PushMessageToClient(sessionConnection.HostInternalSession, msg);
                }
            }
            else 
            {
                // Send private message
                string internalSessionID = OperationContext.Current.Channel.SessionId;
                SessionConnectionInfo sci = sessionConnections.Where(x => (x.HostInternalSession == internalSessionID || x.ConnectedUsernames.ContainsValue(internalSessionID))).FirstOrDefault();

                if (sci != null)
                {
                    if (sci.UserConnected)
                    {
                        if (internalSessionID == sci.HostInternalSession)
                            foreach(string connectedUserInternalSession in sci.ConnectedUsernames.Values)
                                PushMessageToClient(connectedUserInternalSession, msg);
                        else
                            PushMessageToClient(sci.HostInternalSession, msg);
                    }
                    else                
                        PushMessageToClient(internalSessionID,
                                            new GeneralError() {ErrorMessage = "User is no longer connected."});
                }
                else
                {
                    PushMessageToClient(internalSessionID,
                                        new GeneralError() { ErrorMessage = "Session no longer exists.  U is not connected." });
                }
            }
        }

        private void StartSendFile(FileBeginUploadMessage fsm)
        {
            string internalSessionID = OperationContext.Current.Channel.SessionId;
            if (fsm.TotalBytes > 20000000)
                PushMessageToClient(internalSessionID,
                                    new FileDenyMessage() { Reason = "File size Limit of 20 million bytes exceeded." });
            else
                SendMessage(fsm);
        }

        // method to handle messages received.  
        protected override void OnMessage(string sessionId, DuplexMessage data)
        {
            if (data is HostSessionMessage)
                CreateHostSession(data as HostSessionMessage);
            else if (data is JoinSessionMessage)
                JoinSession(data as JoinSessionMessage);
            else if (data is FileBeginUploadMessage)
                StartSendFile(data as FileBeginUploadMessage);
            else
                SendMessage(data);
        }

        // When a client disconnects, if it's a host we remove the session and notify the other user.  When it's not the host, we send
        // a message to the host, but the session remains.
        protected override void OnDisconnected(string sessionId)
        {
            SendMessage(new DisconnectMessage());
            string internalSessionID = OperationContext.Current.Channel.SessionId;
            SessionConnectionInfo sci = sessionConnections.Where(x => (x.HostInternalSession == internalSessionID || x.ConnectedUsernames.ContainsValue(internalSessionID))).FirstOrDefault();

            // host quits
            if (sci.HostInternalSession == internalSessionID)
            {
                foreach (string connectedUserInternalSession in sci.ConnectedUsernames.Values)
                {
                    PushMessageToClient(connectedUserInternalSession, new HostDisconnectedMessage());
                    sessionConnections.Remove(sci);
                }
            }
            else
            {
                sci.Disconnect(internalSessionID); // another user can now join
                PushMessageToClient(sci.HostInternalSession, new ClientDisconnectedMessage());
            }
        }
    }

    // Class that represents a connection between two users.  SessionKey is the random session as seen by the host and connected user.
    // HostInteralSession and ConnectedUserInternalSession are the sessions as defined by: OperationContext.Current.Channel.SessionId
    public class SessionConnectionInfo
    {
        public string HostUserName { get; set; }
        public string HostInternalSession { get; set; }

        public Dictionary<string, string> ConnectedUsernames { get; private set; }
        
        public string SessionKey { get; set; }

        public bool UserConnected
        {
            get { return ConnectedUsernames.Count > 0; }
        }

        public SessionConnectionInfo()
        {
            HostUserName = string.Empty;
            HostInternalSession = string.Empty;

            ConnectedUsernames = new Dictionary<string,string>();
            SessionKey = string.Empty;
        }

        public void Join(string user, string userInternalSession)
        {
            ConnectedUsernames.Add(user, userInternalSession);
        }

        public void Disconnect(string user)
        {
            ConnectedUsernames.Remove(user);
        }
    }
}
